/*
* Copyright 2010 Pavel Kozhin 
*
* Licensed under the Apache License, Version 2.0 (the "License"); 
* you may not use this file except in compliance with the License. 
* You may obtain a copy of the License at 
* 
* http://www.apache.org/licenses/LICENSE-2.0
* 
* Unless required by applicable law or agreed to in writing, software 
* distributed  under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
* See the License for the specific language governing permissions and 
* limitations under the License.
*
* Author: Pavel Kozhin.
*/

package org.bookmarksframework.core
{
	import flash.events.Event;
	import flash.events.EventDispatcher;
	
	import mx.core.IFactory;
	import mx.core.IFlexDisplayObject;
	import mx.core.Singleton;
	import mx.events.FlexEvent;
	
	import org.bookmarksframework.event.FragmentChangeEvent;
	import org.bookmarksframework.event.FragmentEvent;
	import org.bookmarksframework.manager.IBookmarksManager;
	import org.bookmarksframework.rule.IRuleParser;
	import org.bookmarksframework.util.PropertyBinder;
	
	[Event(name="change", type="org.bookmarksframework.event.FragmentChangeEvent")]
	
	[Event(name="register", type="org.bookmarksframework.event.FragmentEvent")]
	
	[Event(name="unregister", type="org.bookmarksframework.event.FragmentEvent")]
	
	[Event(name="commit", type="org.bookmarksframework.event.FragmentCommitEvent")]
	
	[Event(name="update", type="org.bookmarksframework.event.FragmentEvent")]
	
	[Event(name="commitComplete", type="org.bookmarksframework.event.FragmentEvent")]
	
	public class Fragment extends EventDispatcher implements IFragment
	{
		protected var manager:IBookmarksManager;
		
		protected var binding:PropertyBinder = null;
		
		protected var parentResolver:ParentResolver;
		
		private var waitingTarget:Boolean = true;
		
		private var initHandled:Boolean = false;
		
		public function initialize(host:IFlexDisplayObject):void
		{
			if (!host)
				throw new Error("Bookmarks Error: Host can't be null.");
			
			if (registered)
				return;
			
			_host = host;
			
			if (hasEventListener("fragmentHostChanged"))
				dispatchEvent(new Event("fragmentHostChanged"));
			
			host.addEventListener(FlexEvent.PREINITIALIZE, initHandler, false, -1000);
		}
		
		private var cachedParent:IFragment;
		private var _parent:IFragment;
		
		public function get parent():IFragment
		{
			return _parent || cachedParent || 
				(cachedParent = parentResolver.execute());
		}

		public function set parent(value:IFragment):void
		{
			_parent = value;
		}

		private var explicitActive:Boolean = false;
		
		private var _active:Boolean;
		
		public function get active():Boolean
		{
			var result:Boolean = false;
			
			if (explicitActive)
			{
				result = _active;
			}
			else if (parent && (parent.isChildActiveFunction != null))
			{
				result = parent.isChildActiveFunction(this);
			}
			else
			{
				result = host.parent && host.visible && 
					host.parent.visible;
			}
			return result;
		}
		
		public function set active(value:Boolean):void
		{
			_active = value;
			explicitActive = true;
		}
		
		private var _host:IFlexDisplayObject;
		
		[Bindable("fragmentHostChanged")]
		public function get host():IFlexDisplayObject
		{
			return _host;
		}
		
		private var _uid:String;
		
		public function get uid():String
		{
			return _uid;
		}
		
		public function set uid(value:String):void
		{
			_uid = value;
		}
		
		private var _id:String;
		
		public function get id():String
		{
			return _id;
		}
		
		public function set id(value:String):void
		{
			_id = value;
		}
		
		private var _registered:Boolean = false;
		
		public function get registered():Boolean
		{
			return _registered;
		}
		
		public function set registered(value:Boolean):void
		{
			_registered = value;
			
			if (!value)
				cachedParent = null;
			
			var evtType:String = value ? 
				FragmentEvent.REGISTER : FragmentEvent.UNREGISTER;
			
			if (hasEventListener(evtType))
			{
				var evt:FragmentEvent = new FragmentEvent(evtType, this);
				dispatchEvent(evt);
			}
			
			if (value)
				bind();
		}
		
		private var _enabled:Boolean = true;
		
		public function get enabled():Boolean
		{
			return _enabled;
		}
		
		public function set enabled(value:Boolean):void
		{
			_enabled = value;
		}
		
		private var _rule:Object = null;
		
		public function get rule():Object
		{
			return _rule;
		}
		
		public function set rule(value:Object):void
		{
			_rule = value;
		}
		
		private var _targetField:String;
		
		[Bindable]
		public function get targetField():String
		{
			return _targetField;
		}
		
		public function set targetField(value:String):void
		{
			if (_targetField == value)
				return;
			
			if (target && !target.hasOwnProperty(value))
				throw new Error("Target doesn't have property with name \""+value+"\".");
			
			_targetField = value;
			
			if (!registered)
				return;
			
			bind();
		}
		
		private var _target:Object;
		
		[Bindable]
		public function get target():Object
		{
			return _target;
		}
		
		public function set target(value:Object):void
		{
			if (_target == value)
				return;
			
			if (!(value is IFlexDisplayObject))
				throw new Error("Target should implement IFlexDisplayObject.");
							
			if (targetField && !value.hasOwnProperty(targetField))
				throw new Error("Target doesn't have property with name \""+targetField+"\".");
			
			_target = value;
			
			if (value && waitingTarget)
			{
				init();
				waitingTarget = false;
			}
			else if (!waitingTarget)
			{
				parentResolver.init();
			}
			
			bind();
		}	
		
		private var _value:* = null;
		
		[Bindable]
		public function get value():*
		{
			return (binding) ? binding.value: _value;
		}
		
		public function set value(value:*):void
		{
			(binding) ? (binding.value = value) : _value = value;
		}
		
		private var _ruleParser:IFactory = null;
		
		public function get ruleParser():IFactory
		{
			return _ruleParser;
		}
		
		public function set ruleParser(value:IFactory):void
		{
			_ruleParser = value;
		}
		
		private var _path:Path = null;
		
		[Bindable("fragmentPathChanged")]
		public function get path():Path
		{
			if (manager)
			{
				var p:Path = manager.activePath || 
					manager.defaultPath;
				if (p)
				{
					_path = p.getFragment(this);
				}
			}
			
			return _path;
		}

		private var _part:* = null;
		
		[Bindable("fragmentPartChanged")]
		public function get part():*
		{
			return _part;
		}
		
		public function commit(path:*):void
		{
			if (registered)
			{
				manager.commit(path, this);
			}
		}
		
		public function destroy():void
		{
			if (manager)
				manager.unregister(this);
			
			if (binding)
				binding.unbind();
			
			if (host)
				host.removeEventListener(FlexEvent.PREINITIALIZE, initHandler);
			
			if (parentResolver)
				parentResolver.destroy();
			
			binding = null;
			manager = null;
			_host = null;
			_ruleParser = null;
			parentResolver = null;
		}
		
		private var _isChildActiveFunction:Function = null;
		
		[Bindable]
		public function get isChildActiveFunction():Function
		{
			return _isChildActiveFunction;
		}

		public function set isChildActiveFunction(value:Function):void
		{
			_isChildActiveFunction = value;
		}
		
		protected function init():void
		{
			if (!initHandled)
				return;
			
			if (!target)
			{
				waitingTarget = true;
				return;
			}
			
			try {
				manager = 
					IBookmarksManager(Singleton.getInstance("org.bookmarksframework." +
						"manager::IBookmarksManager"));
			} catch (e:Error) {
				throw new Error("Please, check that IBookmarksManager instantiation happens " +
					"at the very beginning and before the first IFragment initialization. Also " +
					"make sure that IBookmarksManager is properly registered via mx.core.Singleton");
			}
			addEventListener(FragmentEvent.COMMIT_COMPLETE, 
				commitCompleteHandler, false, Number.MAX_VALUE);
			addEventListener(FragmentEvent.UPDATE, 
				updateHandler, false, Number.MAX_VALUE);
			
			parentResolver = new ParentResolver(this);
			parentResolver.init();
			manager.register(this);
		}
		
		protected function bind():void
		{
			if (target && targetField)
			{
				if (binding)
					binding.unbind();
				
				binding = new PropertyBinder(target, targetField, bindingUpdated);
				binding.ignoreFirstBinding = true;
				
				binding.bind();
				refreshPart();
				
				if (_value)
					value = _value;
			}
		}
		
		protected function unbind():void
		{
			if (binding)
				binding.unbind();
		}
		
		protected function bindingUpdated(oldValue:Object, newValue:Object):void
		{
			if (!registered)
				return;
			
			var evt:FragmentChangeEvent;
			var oldPart:* = part;
			
			_value = newValue;
			refreshPart();
			
			if ((oldValue !== null) && (newValue !== null) && (oldValue != -1))
			{
				evt = new FragmentChangeEvent(this, oldValue, 
					newValue, oldPart, part);
				dispatchEvent(evt);
			}
		}
		
		protected function refreshPart():void
		{
			if (!ruleParser || !rule || (value === null))
				return;
			
			var oldPath:* = part;
			var newPath:*;
			var parser:IRuleParser = ruleParser.newInstance();
			parser.rule = rule;
			newPath = parser.findPath(value);
			
			if (oldPath != newPath)
			{
				_part = newPath;
				if (hasEventListener("fragmentPartChanged"))
					dispatchEvent(new Event("fragmentPartChanged"));
			}
		}
		
		protected function updateHandler(event:FragmentEvent):void
		{
			if (hasEventListener("fragmentPathChanged"))
				dispatchEvent(new Event("fragmentPathChanged"));
		}
		
		protected function commitCompleteHandler(event:FragmentEvent):void
		{
			if (hasEventListener("fragmentPathChanged"))
				dispatchEvent(new Event("fragmentPathChanged"));
		}
		
		private function initHandler(event:FlexEvent):void
		{
			initHandled = true;
			event.target.removeEventListener(FlexEvent.PREINITIALIZE, initHandler);
			init();
		}
	}
}

import flash.events.EventPhase;

import mx.core.IFlexDisplayObject;

final class ParentResolver {
	
	private var fragment:IFragment;
	
	private var target:IFlexDisplayObject;
	
	public function ParentResolver(fragment:IFragment)
	{
		this.fragment = fragment;
		this.target = fragment.target as IFlexDisplayObject;
	}
	
	public function init():void
	{
		if (target)
			target.removeEventListener(GetParentEvent.GET_PARENT, handler);
		
		target = fragment.target as IFlexDisplayObject;
		
		if (target)
			target.addEventListener(GetParentEvent.GET_PARENT, handler);
	}
	
	public function execute():IFragment
	{
		var evt:GetParentEvent = new GetParentEvent();
		fragment.host.dispatchEvent(evt);
		return evt.fragment;
	}
	
	public function destroy():void
	{
		if (target)
			target.removeEventListener(GetParentEvent.GET_PARENT, handler);
	}
	
	private function handler(event:GetParentEvent):void
	{
		if (event.eventPhase == EventPhase.BUBBLING_PHASE)
		{
			event.stopImmediatePropagation();
			event.fragment = fragment;
		}
	}
}

import flash.events.Event;

import org.bookmarksframework.core.IFragment;

final class GetParentEvent extends Event {
	
	public static const GET_PARENT:String = "org.bookmarksframework.core.GetParentEvent.GET_PARENT";
	
	internal var fragment:IFragment = null;
	
	public function GetParentEvent()
	{
		super(GET_PARENT, true);
	}
}